iT邦幫忙

2025 iThome 鐵人賽

DAY 25
1

經過昨天的升級系統實作,我們的小女巫終於能夠在戰鬥中成長了!今天,我們要來做點比較簡單的東西:

  • 道具掉落系統:讓敵人死亡後有機會掉落各種有用的道具。
  • 受傷閃白效果:利用 PixiJS 濾鏡讓角色受傷時產生視覺回饋。

▸ 道具系統設計概念

在開始程式碼實作之前,讓我們先來思考一下道具系統的設計架構。

  1. 類型化管理:不同種類的道具有不同的效果、外觀、掉落機率。
  2. 自動拾取機制:玩家只要靠近道具就會自動拾取,甚至靠近時就可以讓道具飛向玩家。
  3. 擴展性:就跟敵人、升級類型一樣,應該要考慮讓未來可以輕鬆加入新的道具類型。

為了實現這些功能,我打算建立了三個核心類別:

  • ItemType:定義道具的基本屬性(名稱、效果、外觀等)。就像 EnemyTye 一樣。
  • BaseItem:單個道具實例的行為邏輯。就像 Enemy 一樣。
  • ItemManager:統一管理道具的生成與掉落。

ItemType:道具類型定義

首先讓我們看看道具類型的定義。每種道具都有自己的配置,包含圖案、拾取範圍、掉落權重和使用效果:

// Games/Items/ItemType.ts
import { Game } from "../Game";

export interface IItemConfig {
    code?: string;
    name: string;
    description: string;
    iconFrame: string;
    // 吸取範圍:物品會開始自動飛向小女巫的距離(像素)
    pickUpRange?: number;
    // 掉落權重:數值越高,掉落機率越大
    dropWeight?: number;
    useEffect: (game: Game) => void;
}

export class ItemType {

    static readonly ALL: { [code: string]: ItemType } = {};

    static HealthPotion = new ItemType("health_potion", {
        name: "恢復藥水",
        description: "恢復生命值 20%",
        iconFrame: "health_potion",
        pickUpRange: 0, // 需要直接接觸才能拾取
        dropWeight: 1.0,
        useEffect: (game: Game) => {
            const witch = game.witch;
            const healAmount = Math.ceil(witch.maxHp * 0.2);
            witch.setHp(witch.hp + healAmount, witch.maxHp);
        }
    });

    static ExpOrb = new ItemType("exp_orb", {
        name: "經驗球",
        description: "獲得經驗值",
        iconFrame: "exp_gems",
        pickUpRange: 250, // 較大的吸取範圍
        dropWeight: 3.0, // 較高的掉落權重
        useEffect: (game: Game) => {
            const witch = game.witch;
            witch.gainExp(10); // 給予 10 點經驗值
        }
    });

    constructor(
        readonly code: string,
        readonly config: IItemConfig
    ) {
        config.code = code;
        ItemType.ALL[code] = this;
    }
    
}

這裡我們定義了兩種基本道具:

  • 恢復藥水:需要直接接觸才能拾取,恢復 20% 生命值。
  • 經驗球:有較大的吸取範圍,讓玩家更容易獲得經驗值。

BaseItem:道具行為邏輯

接下來是單個道具的行為實作。這個類別代表道具在舞台畫面上的顯示物件,因此每個道具都會繼承 GameObject,具備碰撞檢測和更新循環:

// Games/Items/BaseItem.ts
import { Game } from "../Game";
import { GameObject } from "../GameObject";
import pixi = CG.Pixi.pixi;
import { ItemType } from "./ItemType";

export class BaseItem extends GameObject {

    constructor(game: Game, private _type: ItemType) {
        super(game);

		this.setSpriteFrame(_type.config.iconFrame);
        this.setHitBox(new PIXI.Circle(0, 0, 16));

    }

    // 物品道具類型
    get type(): ItemType { return this._type }

	/**
	 * 設置物品道具的圖幀。
	 * @param frameName - 物品道具圖幀名稱(參考圖集動畫資源的圖幀列表)
	 */
	setSpriteFrame(frameName: string): void {
		const spritesheet = pixi.assets.getSpritesheet("LittleWitch_TheJourney.圖集動畫.物品道具");
		const texture = spritesheet.textures[frameName];
		this.setSpriteTexture(texture);
	}

    /**
     * 更新循環函數。
     * @param dt - 每幀間隔時間(ms)
     */
    update(dt: number): void {

        const { pickUpRange, useEffect } = this._type.config;
        const witch = this.game.witch;

        // 道具跟著背景往左移動
        this.x -= 0.15 * dt;

        // 檢查是否移出畫面左側,如果是則自動銷毀
        if (this.x < -50) {
            this.destroy({ children: true });
            return;
        }

        if(typeof pickUpRange === "number") {
            const dx = witch.x - this.x;
            const dy = witch.y - this.y;
            const distance = Math.sqrt(dx * dx + dy * dy);

            // 如果在吸取範圍內,開始向小女巫移動
            if(distance < pickUpRange) {
                // 距離越近,移動速度越快
                const speed = 0.9 * (1 - (distance / pickUpRange));
                this.x += dx / distance * speed * dt;
                this.y += dy / distance * speed * dt;
            }
        }

        // 如果碰到小女巫,觸發效果並銷毀物品
        if(this.hitTest(witch)) {
            useEffect(this.game);
            this.destroy({ children: true });
        }
    }
}
  • 自動吸取:經驗球等道具會在一定範圍內自動飛向玩家,越靠近移動速度越快。
  • 碰撞檢測:每個道具預設使用圓形的碰撞箱,就可以使用我們之前建立的 hitTest() 來檢測。
  • 效果觸發:拾取時執行道具配置中定義的 useEffect

由於 "LittleWitch_TheJourney.圖集動畫.物品道具" 是新的資源素材,app.tsstart() 函數裡面也要添加對應的載入程式碼。

ItemManager:道具掉落管理

最後是道具管理器,負責統一處理道具的生成和掉落邏輯:

// Games/Items/ItemManager.ts
import { Game } from "../Game";
import { BaseItem } from "./BaseItem";
import { ItemType } from "./ItemType";

export class ItemManager {
    
    // 所有可掉落的道具類型
    private _droppableItems: ItemType[] = [];
    
    constructor(private _game: Game) {
        this._registerDroppableItems();
    }
    
    /**
     * 註冊所有可掉落的道具類型
     */
    private _registerDroppableItems(): void {
        this._droppableItems.push(
            ItemType.HealthPotion,
            ItemType.ExpOrb
        );
    }
    
    /**
     * 在指定位置掉落隨機道具
     * @param x - X 座標
     * @param y - Y 座標
     * @param dropChance - 基礎掉落機率 (0-1)
     */
    dropRandomItem(x: number, y: number, dropChance: number = 0.1): void {
        if (Math.random() > dropChance) return;
        
        // 根據權重選擇道具類型
        const totalWeight = this._droppableItems.reduce((sum, type) => 
            sum + (type.config.dropWeight || 1), 0);
        
        let random = Math.random() * totalWeight;
        let selectedType: ItemType | null = null;
        
        for (const type of this._droppableItems) {
            random -= (type.config.dropWeight || 1);
            if (random <= 0) {
                selectedType = type;
                break;
            }
        }
        
        if (selectedType) {
            this._createItem(selectedType, x, y);
        }
    }
    
    /**
     * 創建道具實例並添加到場景中
     * @param itemType - 道具類型
     * @param x - X 座標
     * @param y - Y 座標
     */
    private _createItem(itemType: ItemType, x: number, y: number): void {
        const item = new BaseItem(this._game, itemType);
        item.position.set(x, y);
        
        // 添加到遊戲的效果圖層中
        this._game.effectLayer.addChild(item);
    }
}

這個管理器實作了加權隨機掉落系統,讓不同道具有不同的掉落機率。

▸ 敵人死亡道具掉落整合

現在我們需要將道具掉落整合到敵人的死亡流程中。在 Game 類別中加入道具管理器:

// ... (其餘 import 省略)
import { ItemManager } from './Items/ItemManager';

export class Game extends PIXI.Container {
    
    // ... (其餘省略)
    
    private _itemManager: ItemManager;
    
    constructor() {
        super();
        
        // ... (其餘省略)
        
        this._itemManager = new ItemManager(this);
        
    }
    
    // 升級管理器
    get itemManager(): ItemManager { return this._itemManager }
}

然後在敵人的死亡處理中加入道具掉落:

// Games/Characters/Enemys/BaseEnemy.ts
/**
 * 讓敵人死亡並掉落道具。
 * @override 覆寫 BaseCharacter 的死亡函數,添加道具掉落
 */
async death(): Promise<void> {
    if (!this.isAlive) return;
    
    // 在死亡位置掉落道具
    this.game.itemManager.dropRandomItem(this.x, this.y, 0.4);
    
    // 呼叫基底類別的死亡處理
    return super.death();
}

▸ 受傷閃白效果:ColorOverlayFilter 的應用

接下來我們要來實作受傷閃白效果。PixiJS 提供了豐富的濾鏡系統,其中 ColorOverlayFilter 正好適合我們的需求,它用於將一個純色疊加在圖像上面。

PixiJS 的濾鏡是一個額外的套件,如果是在自己的環境運行,需自行安裝 pixijs/filters 擴充套件。

我打算直接加在 BaseCharacter 裡面,讓所有角色受傷都會有閃白效果。

// ... (其餘省略)

export class BaseCharacter extends GameObject {

    // ... (其餘省略)

    // 受傷閃白濾鏡
    private _flashFilter: PIXI.filters.ColorOverlayFilter;

    constructor(game: Game) {
        super(game);
        
        // ... (其餘省略)

        // 初始化白色疊加濾鏡
        const flashFilter = this._flashFilter = new PIXI.filters.ColorOverlayFilter({
            color: 0xFFFFFF // 白色
        });
        // 一開始讓濾鏡不啟用
        flashFilter.enabled = false;
        this.filters = [flashFilter];
    }

    /**
     * 受到傷害。
     * @param damage - 傷害數值
     */
    takeDamage(damage: number): void {
        this.setHp(this._hp - damage);
        this.emit(BaseCharacter.EVENT.HURT, { damage: damage });
        
        // 如果受到傷害
        if(damage > 0) {
            // 且現在沒有啟用濾鏡
            if(!this._flashFilter.enabled) {
                // 啟用濾鏡,並等待 50 毫秒後重設
                this._flashFilter.enabled = true;
                this.game.wait(50).then(() => {
                    this._flashFilter.enabled = false;
                });
            }
        }
    }
}

除了 ColorOverlayFilter 以外,pixijs/filters 還有非常非常多好用的濾鏡,像是陰影、發光、RGB 分離、模糊、老電影濾鏡等等,想要實際測試看看效果的人,可以到 PixiJS 官方示範的網站上去玩玩~

小女巫 道具系統、受傷閃白 預覽

點我查看 Day 25 範例程式碼點我查看最新進度程式碼

▸ 總結

今天我們成功實作了道具掉落系統受傷閃白特效。道具系統讓我們的遊戲更加往 Roguelike 的道路邁進了一步。而受傷閃白特效我個人覺得極大幅度提升了遊戲的回饋感,讓小女巫的魔法彈真的有把敵人幹掉的感覺,而不是輕飄飄地摸到一下就消失了。尤其是針對高血量的敵人,每次受傷的閃白都會讓玩家清楚知道攻擊有效果。

明天,我們要進入這個系列的最高潮了!我們要把最終大魔王放進這個世界,除了高血量,還會有多種攻擊模式。這不僅是對小女巫,對玩家的考驗,也是對我自己的考驗!


上一篇
Day 24:簡易升級系統與技能選擇(二) - 升級選項與邏輯實作
下一篇
Day 26:拯救世界的路上怎麼可以少了大魔王?BOSS 登場!
系列文
用 PixiJS 寫遊戲!告別繁瑣設定,在 Code.Gamelet 打造你的第一個遊戲26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言